package sideeffect

import (
	"context"
	errors2 "errors"
	"github.com/gohobby/deepcopy"
	"github.com/wI2L/jsondiff"
	"gitverse.ru/synapse/kubelatte/pkg/api/common"
	"gitverse.ru/synapse/kubelatte/pkg/observability/logs"
	util2 "gitverse.ru/synapse/kubelatte/pkg/operator"
	"gitverse.ru/synapse/kubelatte/pkg/storage"
	"gitverse.ru/synapse/kubelatte/pkg/util/match"
	"k8s.io/api/admission/v1beta1"
	"strconv"
	"strings"
)

type LauncherI interface {
	StartSideEffect(ctx context.Context, arfields *common.ARFields, patches []jsondiff.Operation) []jsondiff.Operation
}

type ExecutorI interface {
	ApplySideEffect(ctx context.Context, info *SE) error
}

type Launcher struct {
	executor ExecutorI
}

func NewLauncher(client ExecutorI) LauncherI {
	return &Launcher{executor: client}
}

type refs struct {
	triggerName      string
	triggerNamespace string
	sideEffectConfig string
	templateRefs     []string
	recreate         bool
}

func (l *Launcher) StartSideEffect(ctx context.Context, arfields *common.ARFields, patches []jsondiff.Operation) []jsondiff.Operation {
	if util2.KbltSideEffect != "enabled" && util2.KbltSideEffect != "true" {
		logs.Debugf("side effect mode turned off")
		return patches
	}

	logs.Debugf("Side effect starting")

	var mode Mode
	var tag int64

	if arfields.Operation == v1beta1.Create {
		mode = Creation
	}
	if arfields.Operation == v1beta1.Update {
		if util2.SideEffectRecreation != "true" && util2.SideEffectRecreation != "enabled" {
			return patches
		}
		mode = Recreation
		tag = GetTag(ctx, arfields.OldObject) + 1
	}

	info, err := getObjectInfoFromMap(arfields.Object, arfields.Namespace)
	if err != nil {
		logs.Error("failed to get object info:", err.Error())
		return patches
	}
	refers := getTriggerAndTemplateRefs(ctx, arfields)
	if refers == nil {
		logs.Debug("no refs for side effect")
		return patches
	}

	patches = AddTag(arfields.Object, tag, patches)

	go l.sendSideEffect(ctx, refers, mode, tag, info)
	return patches
}

func (l *Launcher) sendSideEffect(ctx context.Context, refers []refs, mode Mode, tag int64, info *SE) {
	for _, ref := range refers {
		if mode == Recreation && !ref.recreate {
			continue
		}
		err := l.executor.ApplySideEffect(ctx, &SE{
			SeMode: int32(mode),
			Configs: Configs{
				Kind:       info.Kind,
				ApiVersion: info.ApiVersion,
				ParentTag:  tag,
				TriggerRef: TriggerRef{
					SideEffectConfig: ref.sideEffectConfig,
					Name:             ref.triggerName,
					Namespace:        ref.triggerNamespace,
				},
				Metadata: Metadata{
					Name:      info.Metadata.Name,
					Namespace: info.Metadata.Namespace,
				},
				TemplateRefs: ref.templateRefs,
			},
		})

		if err != nil {
			logs.Errorf("executing side effect error: %s", err)
		}
	}
}

func getTriggerAndTemplateRefs(ctx context.Context, fields *common.ARFields) []refs {
	triggers := storage.Storage.GetTriggers()
	var references []refs

	for _, trigger := range triggers {
		for i := 0; i < len(trigger.Spec.SideEffectConfigs); i++ {
			seConfig := trigger.Spec.SideEffectConfigs[i]
			if match.IsMustProcess(ctx, *fields, seConfig.Match) {
				references = append(references, refs{
					triggerName:      trigger.Name,
					triggerNamespace: trigger.Namespace,
					sideEffectConfig: seConfig.Name,
					templateRefs:     seConfig.TemplateRefs,
					recreate:         seConfig.Recreate,
				})
			}
		}
	}

	logs.Debugf("got refs: %v", references)
	return references
}

func getObjectInfoFromMap(obj map[string]interface{}, ns string) (*SE, error) {
	var info SE

	info.Kind = obj["kind"].(string)
	info.ApiVersion = obj["apiVersion"].(string)

	name, ok := obj["metadata"].(map[string]interface{})["name"]
	if !ok {
		name, ok = obj["metadata"].(map[string]interface{})["generateName"]
		if !ok {
			return nil, errors2.New("name not found")
		}
	}

	info.Metadata.Name = name.(string)
	info.Metadata.Namespace = ns

	return &info, nil
}

// GetTag gets from annotations annotTag value. Returns -1 if tag was not found
func GetTag(ctx context.Context, obj map[string]any) int64 {
	tmp, ok := obj["metadata"]
	if !ok {
		return -1
	}

	metadata := tmp.(map[string]any)
	tmp, ok = metadata["annotations"]
	if !ok {
		return -1
	}

	annotations := tmp.(map[string]any)
	if tag, ok := annotations[annotTag]; ok {
		c, ok := tag.(string)
		if ok {
			tag, err := strconv.ParseInt(c, 10, 64)
			if err != nil {
				logs.Debugf("parent tag annotation is not int64")
				return -1
			}
			return tag
		}
	}

	return -1
}

func AddTag(oldObj map[string]any, tag int64, patches []jsondiff.Operation) []jsondiff.Operation {
	newObj := deepcopy.DeepCopy(oldObj).(map[string]any)
	metadata := newObj["metadata"].(map[string]any)
	ann, ok := metadata["annotations"]
	if !ok {
		for _, patch := range patches {
			if strings.HasPrefix(patch.Path, "/metadata/annotations") && patch.Type == jsondiff.OperationAdd {
				oldObj["metadata"].(map[string]any)["annotations"] = map[string]any{}
			}
		}
		metadata["annotations"] = map[string]any{}
		ann = metadata["annotations"]
	}

	ann.(map[string]any)[annotTag] = strconv.FormatInt(tag, 10)

	patch, err := jsondiff.Compare(oldObj, newObj)
	if err != nil {
		return patch
	}

	return append(patches, patch...)
}
